/*
 * Copyright (C) 2013 Michael
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package com.jm.jmsql.models;

import com.jm.jmsql.objects.Column.JdbcJavaType;
import com.jm.jmsql.objects.Definition;
import com.jm.jmsql.sql.SQLEditor;
import java.math.BigDecimal;
import java.sql.Array;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Struct;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import javax.swing.event.EventListenerList;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

/**
 *
 * @author Michael L.R. Marques
 */
public class ResultSetTableModel implements TableModel, TableModelListener {
    
    /**
     * 
     */
    private EventListenerList listeners;
    
    /**
     * 
     */
    private Definition definition;
    
    /**
     * 
     */
    private List<Column> columns;
    
    /**
     * 
     */
    private List<Row> rows;
    
    /**
     * 
     */
    private List<Cell> cells;
    
    /**
     * 
     */
    private int totalRows;
    
    /**
     * 
     */
    private int pageSize;
    
    /**
     * 
     */
    private int pages;
    
    /**
     * 
     */
    private int page;
    
    /**
     * 
     * @param definition
     * @throws SQLException 
     */
    public ResultSetTableModel(Definition definition) throws SQLException {
        // Set the result set
        this.definition = definition;
        // Initialize the event listener list
        this.listeners = new EventListenerList();
        // Add this table model listener
        this.listeners.add(TableModelListener.class, this);
        // Create the column data
        this.columns = new ArrayList();
        ResultSetMetaData rsmd = definition.getResults().getMetaData();
        for (int i = 1; i <= rsmd.getColumnCount(); i++) {
            this.columns.add(new Column(rsmd, i));
        }
        // Get total rows
        this.totalRows = definition.getRowCount();
        // Calculate page size
        this.pageSize = this.totalRows <= 0 ? 1 : (this.totalRows < SQLEditor.DEFAULT_PAGE_SIZE ? this.totalRows : SQLEditor.DEFAULT_PAGE_SIZE);
        // Calculate the number of pages
        this.pages = this.pageSize == this.totalRows ? 1 : ((this.totalRows / this.pageSize) + (this.totalRows % this.pageSize > 0 ? 1 : 0));
        // Create the row array
        this.rows = new ArrayList(this.pageSize);
        // Move result set position to beginning of page and load rows
        loadRows(this.pageSize * (this.page = 1) - this.pageSize + 1);
    }
    
    /**
     * 
     * @return 
     */
    public ResultSet getResults() {
        return this.definition.getResults();
    }
    
    /**
     * Set the page to go to
     * @param page 
     * @throws java.sql.SQLException 
     */
    public void setPage(int page) throws SQLException {
        // Check if the page is available
        if (page > 0 &&
                page <= this.pages) {
            // Set the page
            this.page = page;
            // Move result set position to beginning of page, using the next page and load rows
            loadRows(this.pageSize * this.page - this.pageSize + 1);
            // Fire table changed
            fireTableChanged();
        }
    }
    
    /**
     * Set the size of the page
     * @param pageSize 
     * @throws java.sql.SQLException 
     */
    public void setPageSize(int pageSize) throws SQLException {
        // Do not set the page size if the new value is the same as the old value
        if (this.pageSize != pageSize) {
            // Calculate page size
            this.pageSize = this.totalRows < pageSize ? this.totalRows : pageSize == 0 ? 1 : pageSize;
            // Calculate the number of pages
            this.pages = this.pageSize == this.totalRows ? 1 : ((this.totalRows / this.pageSize) + (this.totalRows % this.pageSize > 0 ? 1 : 0));
            // Create the row array
            this.rows = new ArrayList(this.pageSize);
            // Move result set position to beginning of page and load rows
            loadRows(this.pageSize * (this.page = 1) - this.pageSize + 1);
            // Fire table changed
            fireTableChanged();
        }
    }
    
    /**
     * Loads row data from current position in result set
     * @throws SQLException 
     */
    private void loadRows(int position) throws SQLException {
        boolean next = this.definition.getResults().absolute(position);
        // Re-create the row collection
        this.rows = new ArrayList(this.pageSize);
        // Add the rows to the row collection
        for (int i = 1; i <= this.pageSize; i++) {
            // Check if the current record is valid
            if (next) {
                this.rows.add(new Row(this.definition.getResults(), i, this.columns.size()));
            }
            // Check if there is a next record
            if (!(next = this.definition.getResults().next())) {
                return;
            }
        }
    }
    
    /**
     * Reset position in result set to beginning of page and load rows
     * @throws SQLException 
     */
    public void refresh() throws SQLException {
        // Move result set position to beginning of page and load rows
        loadRows(this.pageSize * this.page - this.pageSize + 1);
        // Fire table changed
        fireTableChanged();
    }
    
    /**
     * Move to first page and load rows
     * @throws SQLException 
     */
    public void firstPage() throws SQLException {
        // Check if we are not already on the first page
        if (!isFirstPage()) {
            // Move result set position to beginning of page and load rows
            loadRows(this.pageSize * (this.page = 1) - this.pageSize + 1);
            // Fire table changed
            fireTableChanged();
        }
    }
    
    /**
     * Move to the previous page if not on first page and load rows
     * @throws SQLException 
     */
    public void previousPage() throws SQLException {
        // Check if we have a previous page
        if (hasPreviousPage()) {
            // Move result set position to beginning of page, using the previous page and load rows
            loadRows(this.pageSize * --this.page - this.pageSize + 1);
            // Fire table changed
            fireTableChanged();
        }
    }
    
    /**
     * Move to the next page if not on last page and load rows
     * @throws java.sql.SQLException
     */
    public void nextPage() throws SQLException {
        // Check if we have a next page
        if (hasNextPage()) {
            // Move result set position to beginning of page, using the next page and load rows
            loadRows(this.pageSize * ++this.page - this.pageSize + 1);
            // Fire table changed
            fireTableChanged();
        }
    }
    
    /**
     * Move to last page and load rows
     * @throws java.sql.SQLException
     */
    public void lastPage() throws SQLException {
        // Check if we are not already on the last page
        if (!isLastPage()) {
            // Move result set position to beginning of page, using the last page and load rows
            loadRows(this.pageSize * (this.page = this.pages) - this.pageSize + 1);
            // Fire table changed
            fireTableChanged();
        }
    }
    
    /**
     * The number of pages
     * @return 
     */
    public int getPages() {
        return this.pages;
    }
    
    /**
     * The current page
     * @return 
     */
    public int getPage() {
        return this.page;
    }
    
    /**
     * 
     * @return 
     */
    public int getPageSize() {
        return this.definition.getResults() == null ? 0 : this.pageSize;
    }
    
    /**
     * Are we on the first page
     * @return 
     */
    public boolean isFirstPage() {
        return this.page == 1;
    }
    
    /**
     * Are we on the last page
     * @return 
     */
    public boolean isLastPage() {
        return this.page == this.pages;
    }
    
    /**
     * Is there a next page
     * @return 
     */
    public boolean hasNextPage() {
        return this.page < this.pages;
    }
    
    /**
     * Is there a previous page
     * @return 
     */
    public boolean hasPreviousPage() {
        return this.page > 1;
    }
    
    /**
     * Gets the number of records in the result set
     * @return 
     */
    public int getTotalRows() {
        return this.totalRows;
    }
    
    /**
     * Gets the number of columns in the result set
     * @return 
     */
    public int getTotalColumns() {
        return this.columns.size();
    }
    
    /**
     * 
     * @return 
     */
    public List<Cell> getCells() {
        return this.cells;
    }
    
    /**
     * 
     * @param rowIndices
     * @return 
     */
    public List<Row> getRows(int[] rowIndices) {
        List<Row> rows = new ArrayList(rowIndices.length);
        for (int rowIndex : rowIndices) {
            rows.add(this.rows.get(rowIndex));
        }
        return rows;
    }
    
    /**
     * The number of rows the table can view
     * @return 
     */
    @Override
    public int getRowCount() {
        return this.rows.size();
    }
    
    /**
     * 
     * @return 
     */
    @Override
    public int getColumnCount() {
        return this.columns.size() + 1;
    }
    
    /**
     * 
     * @param columnIndex
     * @return 
     */
    @Override
    public String getColumnName(int columnIndex) {
        return columnIndex == 0 ? "#" : this.columns.get(columnIndex - 1).getName();
    }
    
    /**
     * 
     * @param columnIndex
     * @return 
     */
    @Override
    public Class<?> getColumnClass(int columnIndex) {
        return columnIndex == 0 ? Integer.class : this.columns.get(columnIndex - 1).getType();
    }
    
    /**
     * 
     * @param rowIndex
     * @param columnIndex
     * @return 
     */
    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        //return columnIndex == 0 ? false : true;
        return false;
    }
    
    /**
     * 
     * @param rowIndex
     * @param columnIndex
     * @return 
     */
    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        return columnIndex == 0 ? this.rows.get(rowIndex).getIndex() : this.rows.get(rowIndex).get(columnIndex - 1);
    }
    
    /**
     * 
     * @param newValue
     * @param rowIndex
     * @param columnIndex 
     */
    @Override
    public void setValueAt(Object newValue, int rowIndex, int columnIndex) {
        this.rows.get(rowIndex).set(columnIndex - 1, newValue);
        this.cells.add(new Cell(rowIndex, this.columns.get(columnIndex - 1), newValue));
    }
    
    /**
     * 
     * @param l 
     */
    @Override
    public void addTableModelListener(TableModelListener l) {
        this.listeners.add(TableModelListener.class, l);
    }
    
    /**
     * 
     * @param l 
     */
    @Override
    public void removeTableModelListener(TableModelListener l) {
        this.listeners.remove(TableModelListener.class, l);
    }
    
    /**
     * 
     */
    public void fireTableChanged() {
        // Loop through the listeners
        for (TableModelListener listener : this.listeners.getListeners(TableModelListener.class)) {
            // Lazily create the event:
            listener.tableChanged(new TableModelEvent(this, 0, this.rows.size(), TableModelEvent.ALL_COLUMNS));
        }
    }
    
    /**
     * 
     * @param e 
     */
    @Override
    public void tableChanged(TableModelEvent e) {
        if (this.cells == null) {
            this.cells = new ArrayList();
        } else {
            this.cells.clear();
        }
    }
    
    /**
     * 
     * @author Michael L.R. Marques
     */
    public static class Column {
        
        /**
         * 
         */
        private String name;
        
        /**
         * 
         */
        private Class type;
        
        /**
         * 
         */
        private int length;
        
        /**
         * 
         */
        private int decimal;
        
        /**
         * 
         */
        private int index;
        
        /**
         * 
         */
        private boolean primaryKey;
        
        /**
         * 
         */
        private boolean foreignKey;
        
        /**
         * 
         * @param rsmd
         * @param index
         * @throws SQLException 
         */
        public Column(ResultSetMetaData rsmd, int index) throws SQLException {
            this.name = rsmd.getColumnName(index);                                      // Passes the column name
            this.type = JdbcJavaType.get(rsmd.getColumnType(index)).getType();   // Converts the column type to a java type
            this.length = rsmd.getColumnDisplaySize(index);                             // Gets the column length
            this.decimal = rsmd.getScale(index);                                        // Gets the columns decimal places
            this.index = index -1;                                                      // Sets the columns index
        }
        
        /**
         * 
         * @return 
         */
        public String getName() {
            return this.name;
        }
        
        /**
         * 
         * @return 
         */
        public Class getType() {
            return this.type;
        }
        
        /**
         * 
         * @return 
         */
        public int getLength() {
            return this.length;
        }
        
        /**
         * 
         * @return 
         */
        public int getDecimal() {
            return this.decimal;
        }
        
        /**
         * 
         * @return 
         */
        public int getIndex() {
            return this.index;
        }
        
        /**
         * 
         * @param type
         * @return 
         */
        private static Class jdbcToJavaTypes(int type) {
            switch (type) {
                case Types.DATE:
                case Types.TIME:
                case Types.CHAR:
                case Types.VARCHAR:
                case Types.LONGNVARCHAR: return String.class;
                case Types.NUMERIC:
                case Types.DECIMAL: return BigDecimal.class;
                case Types.BIT: return Boolean.class;
                case Types.INTEGER: return Integer.class;
                case Types.BIGINT: return Long.class;
                case Types.REAL:
                case Types.FLOAT: return Float.class;
                case Types.DOUBLE: return Double.class;
                case Types.CLOB:
                case Types.BLOB:
                case Types.BINARY:
                case Types.VARBINARY:
                case Types.LONGVARBINARY: return byte[].class;
                case Types.ARRAY: return Array.class;
                case Types.STRUCT: return Struct.class;
                case Types.REF: return Ref.class;
                case Types.JAVA_OBJECT: return Class.class;
                default: return Object.class;
            }
        }
        
    }
    
    /**
     * 
     * @author Michael L.R. Marques
     */
    public static class Row implements List<Object> {
        
        //
        private List<Object> list;
        //
        private int index;
        
        /**
         * 
         * @param row 
         * @param columns 
         */
        public Row(int row, int columns) {
            this.list = new ArrayList(columns);
            this.index = row;
            for (int i = 1; i <= columns; i++) {
                this.list.add(null);
            }
        }
        
        /**
         * 
         * @param results
         * @param row
         * @param columns
         * @throws SQLException 
         */
        public Row(ResultSet results, int row, int columns) throws SQLException {
            this.list = new ArrayList(columns);
            this.index = row;
            for (int i = 1; i <= columns; i++) {
                this.list.add(results.getObject(i));
            }
        }
        
        /**
         * 
         * @return 
         */
        public int getIndex() {
            return this.index;
        }
        
        /**
         * 
         * @return 
         */
        @Override
        public int size() {
            return this.list.size();
        }
        
        /**
         * 
         * @return 
         */
        @Override
        public boolean isEmpty() {
            return this.list.isEmpty();
        }
        
        /**
         * 
         * @param o
         * @return 
         */
        @Override
        public boolean contains(Object o) {
            return this.list.contains(o);
        }
        
        /**
         * 
         * @return 
         */
        @Override
        public Iterator<Object> iterator() {
            return this.list.iterator();
        }
        
        /**
         * 
         * @return 
         */
        @Override
        public Object[] toArray() {
            return this.list.toArray();
        }
        
        /**
         * 
         * @param <T>
         * @param a
         * @return 
         */
        @Override
        public <T> T[] toArray(T[] a) {
            return this.list.toArray(a);
        }
        
        /**
         * 
         * @param e
         * @return 
         */
        @Override
        public boolean add(Object e) {
            return this.list.add(e);
        }
        
        /**
         * 
         * @param o
         * @return 
         */
        @Override
        public boolean remove(Object o) {
            return this.list.remove(o);
        }
        
        /**
         * 
         * @param c
         * @return 
         */
        @Override
        public boolean containsAll(Collection<?> c) {
            return this.list.containsAll(c);
        }
        
        /**
         * 
         * @param c
         * @return 
         */
        @Override 
        public boolean addAll(Collection<? extends Object> c) {
            return this.list.addAll(c);
        }
        
        /**
         * 
         * @param index
         * @param c
         * @return 
         */
        @Override
        public boolean addAll(int index, Collection<? extends Object> c) {
            return this.list.addAll(index, c);
        }
        
        /**
         * 
         * @param c
         * @return 
         */
        @Override public boolean removeAll(Collection<?> c) {
            return this.list.removeAll(c);
        }
        
        /**
         * 
         * @param c
         * @return 
         */
        @Override
        public boolean retainAll(Collection<?> c) {
            return this.list.retainAll(c);
        }
        
        /**
         * 
         */
        @Override
        public void clear() {
            this.list.clear();
        }
        
        /**
         * 
         * @param index
         * @return 
         */
        @Override
        public Object get(int index) {
            return this.list.get(index);
        }
        
        /**
         * 
         * @param index
         * @param element
         * @return 
         */
        @Override
        public Object set(int index, Object element) {
            return this.list.set(index, element);
        }
        
        /**
         * 
         * @param index
         * @param element 
         */
        @Override
        public void add(int index, Object element) {
            this.list.add(index, element);
        }
        
        /**
         * 
         * @param index
         * @return 
         */
        @Override
        public Object remove(int index) {
            return this.list.remove(index);
        }
        
        /**
         * 
         * @param o
         * @return 
         */
        @Override
        public int indexOf(Object o) {
            return this.list.indexOf(o);
        }
        
        /**
         * 
         * @param o
         * @return 
         */
        @Override
        public int lastIndexOf(Object o) {
            return this.list.lastIndexOf(o);
        }
        
        /**
         * 
         * @return 
         */
        @Override
        public ListIterator<Object> listIterator() {
            return this.list.listIterator();
        }
        
        /**
         * 
         * @param index
         * @return 
         */
        @Override
        public ListIterator<Object> listIterator(int index) {
            return this.list.listIterator(index);
        }
        
        /**
         * 
         * @param fromIndex
         * @param toIndex
         * @return 
         */
        @Override
        public List<Object> subList(int fromIndex, int toIndex) {
            return this.list.subList(fromIndex, toIndex);
        }
    
    }
    
    /**
     * 
     * @author Michael L.R. Marques
     */
    public static class Cell {
        
        /**
         * 
         */
        private int row;
        
        /**
         * 
         */
        private Column column;
        
        /**
         * 
         */
        private Object value;
        
        /**
         * 
         * @param row
         * @param column
         * @param value 
         */
        public Cell(int row, Column column, Object value) {
            this.row = row;
            this.column = column;
            this.value = value;
        }
        
        /**
         * 
         * @return 
         */
        public int getRow() {
            return this.row;
        }
        
        /**
         * 
         * @return 
         */
        public Column getColumn() {
            return this.column;
        }
        
        /**
         * 
         * @return 
         */
        public Object getValue() {
            return this.value;
        }
    
    }
    
}
